package fr.unice.tinytorrent.objects;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import fr.unice.tinytorrent.Chunk;
import fr.unice.tinytorrent.HashValue;
import fr.unice.tinytorrent.TinyTorrent;
import fr.unice.tinytorrent.api.Client;
import fr.unice.tinytorrent.api.RemoteClient;
import fr.unice.tinytorrent.api.Tracker;
import fr.unice.tinytorrent.exceptions.ChunkNotAvailableException;
import fr.unice.tinytorrent.exceptions.FileIsNotDirectory;
import fr.unice.tinytorrent.utils.FileUtils;
import fr.unice.tinytorrent.utils.MonFiltre;

/**
 * 
 * 
 *
 */
public class ClientImpl implements Client,RemoteClient,Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private File sharedFolder;
	private String trackerUrl;
	private Tracker tracker;
	private String identifier;
	private long up =1000000;
	private long down=1000000;
	
	public ClientImpl(File f, String trackerUrl) throws FileIsNotDirectory,RemoteException,MalformedURLException,NotBoundException
	{
		if(f.isDirectory())
		{
			sharedFolder = f;
			this.trackerUrl = trackerUrl;
			tracker = (Tracker) Naming.lookup(this.trackerUrl);
			this.identifier = new BigInteger(130, new SecureRandom()).toString(32);
		}
		else 
			throw new FileIsNotDirectory(f);
	}



	@Override
	public File share(File file, String trackerURL) {
		
		System.out.println("***********************  Upload *******************************");
		File[] morceaux = FileUtils.createChunks(file, sharedFolder);
		Chunk[] chunks  = new Chunk[morceaux.length];
		HashValue resourceHashValue = new HashValue(file);
	    HashValue[] chunkHashValues = new HashValue[morceaux.length];
	    for(int i=0 ; i<morceaux.length ; i++)
	    {
	    	chunks[i] = new Chunk(morceaux[i],i);
	    	chunkHashValues[i] = new HashValue(Chunk.write(chunks[i],sharedFolder));
	    }
		
	    System.out.println("morceaux chunks de "+file.getName()+" créés");
	    
		TinyTorrent t = new TinyTorrent(trackerURL, file.getName(), resourceHashValue, chunkHashValues);
		
		// informe le tracker du nouveau fichier
		try {
			tracker.register(t.getResourceHashValue(), this);
			tracker.register(t, this);
		} 
		catch (RemoteException e) {e.printStackTrace();}
		
		
		return TinyTorrent.write(t, sharedFolder);
	}

	@Override
	public File download(TinyTorrent torrent) {
		try {
			
			System.out.println("***********************  Téléchargement *******************************");
			System.out.println("Début du téléchargement de la ressource : "+torrent.getResourceName());
			
			// charche les remotes disposant de la ressource
			Set<RemoteClient> seeders = tracker.findClients(torrent.getResourceHashValue());
			
			if(seeders.size() > 1) System.out.println(seeders.size()+ " Seeders ont été trouvés");
			else System.out.println(seeders.size()+ " Seeder a été trouvé");
			
			// liste des index téléchargé
			List<Integer> indexDispos  = new ArrayList<Integer>();
			
			// contient la liste des chunks téléchargé
			List<Chunk> chunksdownloaded = downloadChunks(seeders, torrent.getResourceHashValue(), indexDispos);
			// contiendra la liste des fichiers correspondants aux chunks téléchargés
			File[] listChunksdownloaded = new File[chunksdownloaded.size()];
			
			
			// emplacement du chunk
			File chunksDir = new File(sharedFolder.getAbsolutePath()+File.separator+torrent.getResourceName()+File.separator+"chunks");
			chunksDir.mkdirs();
			for( Chunk c : chunksdownloaded){
                down += c.getPayload().length;
                
			}
			
			for(int i=0 ; i<listChunksdownloaded.length ; i++)
			{
				listChunksdownloaded[i] = Chunk.write(chunksdownloaded.get(i), chunksDir);
				
				// test si le chunk téléchargé est valide ou non
				boolean test = false;
				for(HashValue h :torrent.getChunkHashValues())
				{
					if(h.equals(new HashValue(listChunksdownloaded[i])))
					{
						test = true;
						System.out.println("le chunk"+i+" est bon.");
					}
				}
				if(!test)
					System.out.println("le chunk"+i+" n'a pas été correctement téléchargé");
			}
			
			
			// si on a pu télécharger tous les chunks on peut les fusionner
			// sinon, il faut attendre, ou au moins notifier l'utilisateur
			if(indexDispos.size() == torrent.getChunkHashValues().length)
			{
				// downloadedFile contient le chemin absolu de la ressource
				File downloadedFile = new File(sharedFolder.getAbsolutePath()+File.separator+torrent.getResourceName()+File.separator+torrent.getResourceName()); 
				// création du fichier totale
				FileUtils.merge(downloadedFile, listChunksdownloaded);
				if(!FileUtils.delete(chunksDir))
					System.out.println("impossible de supprimer les morceaux chunks dans : "+chunksDir);
				
				System.out.println(torrent.getResourceName()+" est téléchargé avec succès !");
				return downloadedFile;
			}
			else
				System.out.println("Le fichier ne peut pas etre rassembler car il manque des parties non téléchargées");
		
		} 
		catch (RemoteException e) 				{e.printStackTrace();}
		catch (IOException e) 					{e.printStackTrace();} 
		catch (ChunkNotAvailableException e) 	{e.printStackTrace();}
		
		
		return null;
	}

	
	
	
	/**
	 * télécharge tous les chunks auprès des Seeders disponibles
	 * et remplit une liste des index correspondant aux Chunks telechargés (titre indicatif) 
	 * que l'on peut utiliser pour télécharger les index manquant après.
	 * <br>Cette méthode applique une stratégie pour maximiser le nombre des RemoteClient disposants 
	 * de la ressource demandé
	 * @param seeders tous les RemoteClient connecté disposant de la ressource
	 * @param resourceHashValue la valeur de hashage de la ressource à télécharger
	 * @param indexDispos liste qui mémorise les index des Chunks téléchargé
	 * @return List une liste des Chunks téléchargés
	 * @throws RemoteException
	 * @throws ChunkNotAvailableException 
	 */
	private List<Chunk> downloadChunks(Set<RemoteClient> seeders, HashValue resourceHashValue, List<Integer> indexDispos) throws RemoteException, ChunkNotAvailableException
	{
		
		List<Integer> indChunks = new ArrayList<Integer>();
		List<RemoteClient> remoteChunks = new ArrayList<RemoteClient>(); 
		
		for(RemoteClient seeder :seeders)
		{
			Set<Integer> inds = seeder.findAvailableChunks(resourceHashValue);
			
			for(Integer i :	inds)
			{
				// mémorise tous les index et remoteClient trouvé 
				remoteChunks.add(seeder);
				indChunks.add(i);
				// on mis à jour la liste finale des index trouvé (index sans répétition)
				if(indexDispos.indexOf(i) == -1)
					indexDispos.add(i);
			}
		}
		
		// on cherche les chunks les plus rares
		doOccurrenceSort(indChunks, remoteChunks);
		// on assure le téléchargement depuis les clients disposant de moins de chunks
		doChunksNbrSort(indChunks, remoteChunks, resourceHashValue);
		
		return leechChunks(indChunks, remoteChunks, resourceHashValue);
	}
	
	
	/**
	 * Tri la liste indexChunk-remoteChunk par ordre de rareté
	 * @param indChunks Liste contenant les index des chunks
	 * @param remoteChunks liste contenant les remotes disposant du chunk qui lui correspond dans la liste indChunks
	 */
	public void doOccurrenceSort(List<Integer> indChunks, List<RemoteClient>remoteChunks)
	{
		for(int i=0 ; i<indChunks.size()-1 ; i++)
		{
			if(Collections.frequency(indChunks, indChunks.get(i))==1);
			for(int j=i+1 ; j<indChunks.size() ; j++)
			{
				if(Collections.frequency(indChunks, indChunks.get(i)) > Collections.frequency(indChunks, indChunks.get(j)))
				{
					// on remonte le chunk le plus rare dans les premiers rang
					Collections.swap(indChunks, i, j);
					Collections.swap(remoteChunks, i, j);
				}
			}
		}
	}
	
	
	/**
	 * tri le la liste indexChunk-remoteChunk par ordre des clients disposants de moins de ressource
	 * <br> afin d'assurer le téléchargement du maximum des chunks 
	 * <br> aussi cette fonction essaye à chaque fois prendre un remoteClient qui n'a pas encore partagé
	 * <br> cette approche est très utile lors de l'utilisation des Thread 
	 * @param indChunks indChunks Liste contenant les index des chunks
	 * @param remoteChunks liste contenant les remotes disposant du chunk qui correspond à l'index dans la liste indChunks
	 * @param resourceHashValue la valeur de Hashage de la ressource
	 * @throws RemoteException 
	 */
	public void doChunksNbrSort(List<Integer> indChunks, List<RemoteClient> remoteChunks, HashValue resourceHashValue) throws RemoteException
	{
		List<Integer> iTmp = new ArrayList<Integer>();
		boolean test =true;
		for(int i=0 ; i<indChunks.size()-1 ; i++)
		{
			for(int j=i+1 ; j<indChunks.size() ; j++)
			{
				if(indChunks.get(i).equals(indChunks.get(j)))
				{
					if(!remoteChunks.get(i).equals(remoteChunks.get(j)))
					{
						if(iTmp.indexOf(i) == -1)
						{
							if(test)
							{
								iTmp.add(i);
								test=false;
							}
							else
							{
								iTmp.add(j);
								test = true;
							}
						}
					}
				}
			}
		}
		// pour eviter tous les problème lors de la supprission des index-remote inutiles
		Collections.sort(iTmp);
		Collections.reverse(iTmp);
		for(Integer i :iTmp)
		{
			// supprission des doublons
			indChunks.remove(i.intValue());
			remoteChunks.remove(i.intValue());
		}
	}
	
	
	/**
	 * télécharge les chunks à partir de la liste pré-remplit des index et remoteClient
	 * <br>et retourne la liste des chunks téléchargés 
	 * @param indChunks indChunks indChunks Liste contenant les index des chunks
	 * @param remoteChunks liste contenant les remotes disposant du chunk qui correspond à l'index dans la liste indChunks
	 * @param resourceHashValue
	 * @return chunksdownloaded liste des chunks téléchargés
	 * @throws RemoteException
	 * @throws ChunkNotAvailableException
	 */
	public List<Chunk> leechChunks(List<Integer> indChunks, List<RemoteClient> remoteChunks, HashValue resourceHashValue) throws RemoteException, ChunkNotAvailableException
	{
		List<Chunk> chunksdownloaded = new ArrayList<Chunk>();
		
		for(int i=0 ; i<indChunks.size() ; i++)
		{
			System.out.println("téléchargement du chunk"+indChunks.get(i)+" depuis le client : "+remoteChunks.get(i).getIdentifier());
			chunksdownloaded.add(remoteChunks.get(i).leech(resourceHashValue, indChunks.get(i)));
		}
		return chunksdownloaded;
	}
	
	
	
	@Override
	public Chunk leech(HashValue resourceHashValue, int chunkIndex) throws RemoteException, ChunkNotAvailableException {
		FilenameFilter filtre = new MonFiltre(new String[]{"tinytorrent"}, "que les fichiers tinytorrent");
		File[] fichiers = sharedFolder.listFiles(filtre);
		for(File f :fichiers)
		{
			TinyTorrent t = TinyTorrent.read(f);
			if(t.getResourceHashValue().equals(resourceHashValue))
			{
				File fchunk = new File(sharedFolder.getAbsolutePath()+"/"+t.getResourceName()+".chunk"+chunkIndex);
				if(fchunk.exists())
				{
					up += fchunk.length();
					return new Chunk(fchunk, chunkIndex);
				}
				
				
			}
		}
		
		return null;
	}

	@Override
	public Set<Integer> findAvailableChunks(HashValue resourceHashValue) throws RemoteException {
		
		Set<Integer> indexDispos = new HashSet<Integer>();
		File[] fichiers = sharedFolder.listFiles();
		for(File f :fichiers)
		{
			if(f.canRead() && !f.isDirectory())
			{
				if(f.getName().endsWith("tinytorrent"))
				{
					TinyTorrent t = TinyTorrent.read(f);
					if(t.getResourceHashValue().equals(resourceHashValue))
					{
						
						
						for(File fs :fichiers)
						{
							if(fs.getName().contains(".chunk"))
							{
								int index = fs.getName().lastIndexOf(".");
								// contient le nom de la ressource du chunk sans le ".chunk"
								String nomChunk = fs.getName().substring(0, index+6);
								
								if(nomChunk.equals(t.getResourceName()+".chunk"))
								{
									// contient l'index mais en String
									String indexDispo = fs.getName().substring(index+1, fs.getName().length());
									// on extrait l'index
									String resIndex = indexDispo.replace("chunk", "");							
									int ind = Integer.parseInt(resIndex);
									indexDispos.add(ind);
									
									
								}
							}
							
						}
						return indexDispos;			
					}
				}
			}
		}
		
		return indexDispos;
	}

	@Override
	public String getIdentifier() throws RemoteException {
		
		return this.identifier;
	}
	
	
	
	public String getTrackerUrl() {
		return trackerUrl;
	}



	public void setTrackerUrl(String trackerUrl) {
		this.trackerUrl = trackerUrl;
	}



	public void setSharedFolder(File sharedFolder) {
		this.sharedFolder = sharedFolder;
	}



	@Override
	public File getSharedFolder() {
		return sharedFolder;
	}




	@Override
	public Double getRatio() throws RemoteException {

		Double db1 = new Double(up);
		Double db2 = new Double(down);
		
		if(down<=0)
			return 0.0;
		
		Double res = db1/db2;
		return res;
	}



	@Override
	public long getUp() {
		return up;
	}


	@Override
	public long getDown() {
		return down;
	}
	@Override
	public String UptoString() {
		return FileUtils.getFormattedFileSize(up);
	}


	@Override
	public String DowntoString() {
		return FileUtils.getFormattedFileSize(down);
	}

	public Tracker getTracker() {
		return tracker;
	}

}
